import React from "react";
import BaseView from "./BaseView";
import { Bi, Ei } from "@/module-00-codebase/pkg-01-container";
import {
  asyncOne,
  aw,
  isAsync,
  Throwable,
} from "@/module-00-codebase/pkg-02-tool";
export default abstract class BaseElmt<
  T extends any[] = any[],
  THIS extends BaseElmt<T> = any
> extends BaseView<ElmtProps<T, THIS>> {
  private __cacheRenderView: JSX.Element | null = null;
  constructor(props: RealProps<T, THIS>);
  constructor(props: EachProps<T, THIS>);
  constructor(_props: ElmtProps<T, THIS>) {
    super(_props);
    this.__appendenceDom = (
      <Appendence
        params={[this]}
        init={(elmt) => (this.__appendenceObj = elmt)}
      />
    );
    if (this.isForeachComponent()) {
      // skip: virtual element
    } else {
      const props = this.props as RealProps<T, THIS>;
      if (
        this.elmtInit !== BaseElmt.prototype.elmtInit ||
        (props.init && isAsync(props.init))
      ) {
        (async () => {
          try {
            // Delay executing for owned properties initializing.
            // such as private/public isOpened = false;
            await aw(0);
            await this.elmtInit();
            if (!!props.init) await props.init(this as any);
          } catch (ex) {
            const e = ex instanceof Error ? ex : new Error(JSON.stringify(ex));
            this.__error = e;
          } finally {
            this.__inited = true;
            this.elmtRefresh();
          }
        })();
      } else {
        if (props.init && !isAsync(props.init)) {
          props.init(this as any);
        }
        this.__inited = true;
      }
    }
  }
  private __appendenceDom: JSX.Element;
  private __appendenceObj: Appendence | null = null;
  private __appendenceInn: JSX.Element | null = null;
  private __error: Error | null = null;
  private __inited: boolean = false;
  private __stateData: any = {};
  //   private __stateCache = [] as { [key: string]: any }[];
  public abstract elmtView(): JSX.Element | null;
  public async elmtInit() {}
  public elmtRead(key: string): any {
    return Bi.utils.readValue(this.__stateData, key);
  }
  public elmtWrite(key: string, value?: any): void {
    Bi.utils.writeValue(this.__stateData, key, value);
    // this.__stateCache.push({ key, value });
    asyncOne(
      async () => {
        await aw(0);
        if (this.__inited && !this.isDestroyed) {
          //   const delta = {};
          //   this.__stateCache.splice(0, this.__stateCache.length).forEach(ass => Bi.utils.writeValue(delta, ass.key, ass.value));
          this.setState(Bi.utils.deepCopy(this.__stateData));
        }
      },
      this,
      "elmtWrite"
    );
  }
  public elmtRefresh() {
    this.elmtWrite("any", Bi.utils.uuid());
  }
  public render() {
    try {
      if (this.isForeachComponent()) {
        return this.renderForeach();
      } else {
        if (!!this.__error) {
          throw this.__error;
        } else if (!this.__inited) {
          Bi.log.debug(`Initing: ${this.constructor.name}`);
        } else if ((this.props as RealProps<T, THIS>).enabled === false) {
          this.__cacheRenderView = null;
        } else {
          this.__cacheRenderView = this.renderReal();
        }
      }
    } catch (ex) {
      const e = ex instanceof Error ? ex : new Error(JSON.stringify(ex));
      Bi.log.error(e.message);
      this.__cacheRenderView = this.renderError(e);
    }
    return this.assignAppendence(this.__cacheRenderView || <></>);
  }
  private renderForeach() {
    const children4each = childrenToArray(this.props.children);
    const { params4each, key4each, enabled4each, init4each } = this
      .props as EachProps<T, THIS>;
    return (
      <>
        {params4each.map((item, index, arry) => {
          const pack = { item, index, arry };
          const params = item;
          const children = children4each.map((child, index) => {
            if ((child as any)?.props?.params4each === params4each) {
              const eachChild = child as any;
              const ChildType = eachChild.type;
              const childProps = Object.assign(
                { ...eachChild.props },
                { params4each: [params4each[index]] }
              );
              return (
                <ChildType {...childProps} key={`chd_${index}`}>
                  {eachChild.props.children}
                </ChildType>
              );
            } else if (typeof child === "function") {
              return child(pack);
            } else if ([null, undefined, ""].includes((child as any)?.type)) {
              return child;
            } else {
              return React.cloneElement(child as any, { key: `chd_${index}` });
            }
          });
          const eachProps = {
            params,
            key: key4each ? key4each(pack) : pack.index,
          } as any;
          if (init4each) eachProps.init = init4each(pack);
          if (enabled4each) eachProps.enabled = enabled4each(pack);
          if (this.props.className) eachProps.className = this.props.className;
          if (this.props.style) eachProps.style = this.props.style;
          return <this.constructor {...eachProps}>{children}</this.constructor>;
        })}
      </>
    );
  }

  protected renderReal() {
    return this.assignCommons(this.elmtView() || <></>);
  }
  protected renderError(e: Error) {
    return this.assignCommons(<Ei.Text params={[this.renderErrorMsg(e)]} />);
  }
  protected renderErrorMsg(e: Error) {
    return `${this.constructor.name} Error: ${e.message}`;
  }
  protected assignCommons(
    view: JSX.Element,
    className = toClassName(this.props.className),
    style = this.props.style,
    onClick = this.props.onClick,
    index = -1
  ) {
    if (
      (!className && !style && !onClick) ||
      [null, undefined, ""].includes(view.type)
    ) {
      //skip
    } else if (view.type === (<></>).type) {
      const children = childrenToArray(
        view.props.children
      ).map((child, index) =>
        this.assignCommons(child as any, className, style, onClick, index)
      );
      view = React.cloneElement(view, toAssignProps(view, {}, index), children);
    } else {
      view = React.cloneElement(
        view,
        toAssignProps(view, {}, index, className, style, onClick)
      );
    }
    return view;
  }
  public setAppendence(appendence: JSX.Element | null) {
    appendence = appendence || null;
    if (appendence !== this.__appendenceInn) {
      this.__appendenceInn = appendence;
      if (this.__appendenceObj !== null) {
        this.__appendenceObj.elmtRefresh();
      }
    }
  }
  public getAppendence() {
    return this.__appendenceInn;
  }
  private assignAppendence(view: JSX.Element): JSX.Element {
    if (this.constructor === Appendence) {
      return view;
    } else {
      const viewProps = { ...view.props };
      delete viewProps["children"];
      return (
        <view.type {...viewProps}>
          {view.props.children}
          {this.__appendenceDom}
        </view.type>
      );
    }
  }
  private isForeachComponent() {
    const eachProps = this.props as EachProps<T, THIS>;
    return Array.isArray(eachProps?.params4each);
  }
  protected getParams(): T;
  protected getParams<Index extends Extract<keyof T, Number>>(
    n: Index
  ): T[Index];
  protected getParams<Index extends Extract<keyof T, Number>>(n?: Index) {
    const realProps = this.props as RealProps<T, THIS>;
    if (arguments.length === 0) {
      return (realProps.params || []) as T;
    } else if (n !== undefined) {
      return realProps.params[n];
    } else {
      Throwable("Impossible Error: arg0 is not empty and not a number.");
    }
  }
  public static EACH_PARAMS = [] as any[];
}
const childrenToArray = (children: any) => {
  if (Array.isArray(children)) {
    return [...children];
  } else {
    return [children];
  }
};
const toClassName = (clazz: any) =>
  !clazz
    ? undefined
    : typeof clazz === "string"
    ? clazz
    : Bi.styleRenderer.render(clazz);
const toAssignProps = (
  view: any,
  subProps: any,
  index: number,
  className?: string,
  style?: any,
  onClick?: () => void | Promise<void>
) => {
  const classes = [toClassName(view.props.className), className].filter(
    Boolean
  );
  const styles = [view.style, style].filter(Boolean);
  const clicks = [onClick, view.onClick].filter(Boolean);
  const asyncc = clicks.reduce((a, b) => a || isAsync(b), false);
  if (classes.length > 0) subProps.className = classes.join(" ");
  if (styles.length > 0)
    subProps.style = styles.reduce((a, b) => Object.assign(a, b), {});
  if (index > -1) subProps.key = view.key || `m_${index}`;
  if (clicks.length > 0) {
    if (clicks.length === 1) {
      subProps.onClick = clicks[0];
    } else if (asyncc) {
      subProps.onClick = async () => {
        for (let i = 0; i < clicks.length; i++) {
          await clicks[i]();
        }
      };
    } else {
      subProps.onClick = () => {
        for (let i = 0; i < clicks.length; i++) {
          clicks[i]();
        }
      };
    }
  }
  return subProps;
};
class Appendence extends BaseElmt<[BaseElmt<any>]> {
  public elmtView(): JSX.Element | null {
    return this.getParams(0).getAppendence();
  }
}
export type Pack<T> = { item: T; index: number; arry: T[] };
export interface RealProps<T extends any[], ComponentType extends BaseElmt<T>> {
  params: T;
  init?:
    | ((ref: ComponentType) => void)
    | ((ref: ComponentType) => Promise<void>);
  enabled?: boolean;
}
export interface EachProps<T extends any[], ComponentType extends BaseElmt<T>> {
  params4each: T[];
  init4each?: (pack: Pack<T>) => (ref: ComponentType & any) => void;
  enabled4each?: (pack: Pack<T>) => boolean;
  key4each?: (pack: Pack<T>) => number | string;
}
export interface CommonProps {
  className?: ClassName;
  style?: { [key: string]: any };
  onClick?: () => void | Promise<void>;
}
export type ClassName =
  | string
  | { [key: string]: any }
  | Array<ClassName>
  | null;
export type ElmtProps<T extends any[], Elmt extends BaseElmt<T>> = (
  | RealProps<T, Elmt>
  | EachProps<T, Elmt>
) &
  CommonProps;
